package parser;

import com.wang.define.Entities;
import com.wang.define.WellFormException;
import com.wang.define.XmlChar;
import lexer.Lexer;
import lexer.Token;
import lexer.Word;
import static lexer.Tag.*;
import org.apache.log4j.Logger;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

/**
 * User: bohao.wang
 * Date: 12-4-4
 * Time: 3:57
 */
public class Parser {
    public static final Logger logger = Logger.getLogger(Parser.class.getName());

    private Lexer lex;
    private Token look;
    private boolean standalone = true;
    private boolean indtd = true;
    private boolean inAttlist = false;
    public Entities entities;

    public Parser(Lexer l) throws IOException {
        lex = l;
        entities = new Entities();
        move();
    }

    public boolean isStandalone() {
        return standalone;
    }

    public  void move() throws IOException {
        look = lex.scan();
    }

    void error(String s) {
        throw new Error("near line " + lex.line + ": " + s);
    }

    // well form constraint
    void wfc(String s) {
        throw new WellFormException("near line " + lex.line + ": " + s);
    }

    void match(int t) throws IOException {
        if(look.tag == t)move();
        else error("syntax error");
    }

    void match(int t, String addition) throws IOException {
        if(look.tag == t) move();
        else error(addition);
    }

    void match(String sb) throws IOException {
        if(look.tag == id && ((Word)look).lexeme.equals(sb))
            move();
        else error("syntax error");
    }



    int jumpSpace() throws IOException {
        if(look.tag == separator) {
            move();
            return 1;
        }
        return 0;
    }

    // todo
    // prolog ::= XMLDecl? Misc* (doctypedecl Misc*)?
    public void document() throws IOException {
        xmldecl();
        misc();
        doctypedecl();
        // 实体引用需要检查
        indtd = false;
//        fixedEntityReference();

        misc();
        element();

        try {
            misc();
            if(look.tag != separator)error("invalid syntax");
        } catch (RuntimeException e) {
            logger.info("---- END OF FILE ----");
        }
    }

    // xmldecl -> <?xml version="" encoding? sddecl? s? ?>
    void xmldecl() throws IOException {
        if(look.tag != XML)return;
        logger.info("====xml decl start ");
        match(XML);

        while(look.tag != qend) {
            match(separator, "miss S");
            if(look.tag == qend)break;
            logger.info(look);
            String name = look.toString();
            match(id); jumpSpace();
            match('='); jumpSpace();
            int a = 0;
            if(look.tag == '\'' || look.tag == '\"') {
                a = look.tag;
                move();
            }
            else error("miss \' or \"");
            String value = look.toString();
            match(id);
            if(name.equals("standalone")) {
                if(value.equals("yes")) standalone = true;
                else if(value.equals("no")) standalone = false;
                else error("standalone != yes/no");
            }
            match(a);
        }
        match(qend);
        logger.info("====xml decl end  ");
    }

    void element() throws IOException {
        if(look.tag != '<')return;
        logger.info("====element begin ");
        match('<');
        logger.info("\t<" + look + ">");

        // tagName对后面的进行核对
        String tagName = look.toString();
        match(id);
        Set<String> set = new HashSet<String>();
        while(look.tag != '>' && look.tag != emptyEnd) {
            int sCount = jumpSpace();
            if(look.tag == '>' || look.tag == emptyEnd)
                break;
//            logger.info("[Attribute name] " + look);
// wcf Unique Att Spec
            if(set.contains(look.toString()))
                wfc("Unique Att Spec");
            else set.add(look.toString());
// wcf Unique Att Spec
            if(sCount == 0) error("miss space");
            match(id);
            jumpSpace();
            match('=');
            jumpSpace();
            attValue();
        }
        if(look.tag == emptyEnd) {
            logger.info("\t</" + tagName + ">  EMPTY");
            match(emptyEnd);
        }
        else if(look.tag == '>') {
            match('>');
            content();
            match(end);
            logger.info("\t</" + look + ">");
            if(!look.toString().equals(tagName))
                wfc("Well-Formedness Constraint: Element Type Match");
            match(tagName);
            jumpSpace();
            match('>');
        }
        logger.info("====element end");
    }

    void attValue() throws IOException{
        inAttlist = true;
        char tag = '"';
        if(look.tag == '\'') tag = '\'';

        match(tag);
        while(look.tag != tag )  {
            switch (look.tag) {
                case '<': error(" < ");
                    break;
                // Fiexed 后面的实体引用需要记录
                // 在dtd结束以后调用fixedEntityReference()函数进行判断
                case '&':
                    reference();
                    break;
                default: move();
            }
        }
        match(tag);
        inAttlist = false;
    }

    String reference() throws IOException {
        match('&');
        // entity reference
        if (look.tag == id) {
            String referedEntityName = look.toString();
            match(id);
            match(';');
            if(entities.isUnparsed(referedEntityName))
                wfc("Well-Formedness Constraint: Parsed Entity");
            // 普通实体在dtd外面引用需要声明
            // ！只有被引用的内部实体才需要检查，而且是直接或者间接的
            // 外部实体可以忽略
            // 但有一个特殊情况就是在 Attlist里面的默认值，需要预先存下来检查
            if(standalone && !indtd && !entities.hasDeclared(referedEntityName))
                wfc("Well-Formedness Constraint: Entity Declared " + referedEntityName);

            // 属性表里面不能包含外部实体引用
            if(inAttlist && entities.hasExternal(referedEntityName))
                wfc("Well-Formedness Constraint: No External Entity References " + referedEntityName);

            // 属性表里面的实体的替换文本不能包含<
            if(inAttlist && entities.hasLt(referedEntityName))
                wfc("Well-Formedness Constraint: No < in Attribute Values " + referedEntityName);

            referedEntityName = "&" + referedEntityName +";" ;
            return referedEntityName;
        }
        else{ // char reference
            match('#');
//            logger.info("&#" + look + ";");
            String digit = look.toString();
            int value = 0;
            // &#00; dec
            if (digit.matches("(\\d)+"))
                value = Integer.valueOf(digit, 10);
            // &#x0000; hex
            else if(digit.matches("x[0-9a-fA-F]+"))
                value = Integer.valueOf(digit.substring(1), 16);
            else
                error("");
            match(id);
            match(';');
            return String.valueOf((char)value);
        }
    }

    // pereference = % Name ;
    // pe 必须先声明后引用
    String pereference() throws IOException {
        match('%');
        String answer = look.toString();
        if(standalone && !entities.hasDeclared(answer))
            wfc("Well-Formedness Constraint: Entity Declared");
        match(id);
        match(';');
        answer = "%" + answer + ";";
        return answer;
    }

    // PI = <? PITarget .. '?>'
    void pi() throws IOException {
        logger.info("==== pi");
        match(pi, "pi");
        String piTarget = look.toString();
        logger.info(piTarget);
        if(piTarget.equalsIgnoreCase("xml"))
            error("pi's name = (xml)i");
        match(id);
        while(look.tag != qend) {
            move();
        }
        match(qend);
        logger.info("====pi end ");
    }

    // comment = <!-- ... -->
    void comment() throws IOException {
        logger.info("====comment start ");
        match(comment);
        StringBuilder b = new StringBuilder();
        while(look.tag != commentEnd) {
            b.append(look.toString());
            move();
            int index = b.indexOf("--");
            if( index != -1 ) {
                if(index != b.length() - 2)
                    error("bad comment");
                else if(look.tag != '>') {
                    error("bad comment");
                }
                else break;
            }
        }
        if(look.tag == '>') match('>');
        else match(commentEnd);
        logger.info("====comment end ");
    }

    // CharData ::= [^<&]* - ([^<&]* ']]>' [^<&]*)
    // 只要不含< & 和 ]]> 就可以了
    void chardata() throws IOException {
        if(look.tag == cdend) error("invalid ]]>");
        move();
    }

    void cdsect() throws IOException {
        logger.info("==== cdata start");
        match(cdstart);
        while(look.tag != cdend) {
            move();
        }
        match(cdend);
        logger.info("==== cdata end");
    }

    void content() throws IOException {
        while(look.tag != end) {
            switch (look.tag) {
            case '<':
                element(); break;
            case '&':
                reference(); break;
            case XML:
            case pi: // <?
                pi(); break;
            case comment:
                comment(); break;
            case cdstart:
                cdsect(); break;
            default:
                chardata();
            }
        }
    }

    // Misc = comment | PI | s
    void misc() throws IOException {
        int t = look.tag;
        while(t == comment || t == pi || t == separator) {
            switch (look.tag) {
                case comment:
                    comment(); break;
                case  pi:
                    pi(); break;
                case  separator:
                    move(); break;
            }
            t = look.tag;
        }
    }

    // doc = <!DOCTYPE S Name (S ExternalID)? S? ('[' (markupdecl | PEReference | S)* ']' S? )? >
    void doctypedecl() throws IOException {
        if(look.tag != doctype) return;
        logger.info("==== doctype start");
        match(doctype);
        match(separator);
        match(id);
        if(look.tag == separator) {
            move();
            if(look.tag == id) {
                externalId();
            }
        }
        jumpSpace();
        switch (look.tag) {
            case '[':
                match('[');
//      markupdecl ::= elementdecl | attlistdecl | EntityDecl | NotationDecl | PI | Comment
//      PEReference = '%' Name ';'
                while (look.tag != ']') {
                    switch (look.tag) {
                        //todo
                        case '%':
                            pereference(); break;
                        case separator:
                            move(); break;
                        case elementdecl:
                            elementdecl(); break;
                        case attlistdecl:
                            attlistdecl(); break;
                        case pi:
                            pi(); break;
                        case comment:
                            comment(); break;
                        case entitydecl:
                            entitydecl(); break;
                        case notationdecl:
                            notationdecl(); break;
                        default:
                            error("doctypedecl []");
                            break;
                    }
                }
                match(']'); match('>');
                break;
            case '>':
                match('>');
                break;
            default: error("bad doctypedecl");
        }
        logger.info("==== doctype end");
    }

    // externalId = 'SYSTEM' S SystemLiteral | 'PUBLIC' S PubidLiteral S SystemLiteral
    String externalId() throws IOException {
        StringBuilder sb = new StringBuilder();
        if (look.toString().equals("SYSTEM")) {
            match(id);
            match(separator);
            sb.append(systemLiteral());
        }
        else if (look.toString().equals("PUBLIC")) {
            match(id);
            match(separator);
            pubidLiteral();
            match(separator);
            sb.append(systemLiteral());
        }
        else error("bad externalId");
        return sb.toString();
    }

    String systemLiteral() throws IOException {
        char tag = '"';
        if(look.tag == '\'')tag = '\'';
        match(tag);
        StringBuilder sb = new StringBuilder();
        while(look.tag != tag) {
            sb.append(look.toString());
            move();
        }
        match(tag);
        return sb.toString();
    }

    void pubidLiteral() throws IOException {
        systemLiteral();
    }

    // elementdecl = <!Element s name s contentspec s? >
    void elementdecl() throws IOException {
        logger.info("==== elementdecl start");
        match(elementdecl);
        match(separator);
        logger.info(look);
        match(id);
        match(separator);
        contentspec();
        jumpSpace();
        match('>');
        logger.info("==== elementdecl end");
    }

    // contentspec = EMPTY | ANY | MIXER | CHILDREN
    void contentspec() throws IOException {
        if(look.tag == id) {
            if(look.toString().equals("EMPTY") || look.toString().equals("ANY")) {
                match(id);
            }
            else error("empty or any");
        }
        else if(look.tag == '(') {
            match('('); jumpSpace();
            // Mixer
            if(look.tag == '#') {
                match('#');
                logger.info("#PCDATA");
                match("PCDATA");
                jumpSpace();
                if(look.tag == '|') {
                    while (look.tag != ')') {
                        match('|');
                        jumpSpace();
                        logger.info(look);
                        match(id);
                        jumpSpace();
                    }
                    match(')');
                    match('*', "miss *");
                }
                else if(look.tag == ')') {
                    match(')');
                }
                else error("mixed");
            }
            // children
            else if(look.tag == '(' || look.tag == id) {
                children();
            }
        }
    }

    // children = (choice | seq)(?,*,+)?
    void children() throws IOException {
        int no = 0;
        if (look.tag == id) {
            logger.info(look);
            match(id);
            if (look.tag == '?' || look.tag == '*' || look.tag == '+')
                move();
        }
        else {
            seqOrChoice();
        }
        while (look.tag != ')') {
            jumpSpace();
            switch (look.tag) {
                case ',':
                    if (no == 2) error(",");
                    else no = 1;
                    match(',');
                    jumpSpace();
                    if (look.tag == id) {
                        logger.info(look);
                        match(id);
                        if (look.tag == '?' || look.tag == '*' || look.tag == '+')
                            move();
                    }
                    else {
                        seqOrChoice();
                    }
                    break;
                case '|':
                    if (no == 1) error(",");
                    else no = 2;
                    match('|');
                    jumpSpace();
                    if (look.tag == id) {
                        logger.info(look);
                        match(id);
                        if (look.tag == '?' || look.tag == '*' || look.tag == '+')
                            move();
                    }
                    else {
                        seqOrChoice();
                    }
                    break;
                default:
                    error("seq or choice");
            }
        }
        match(')');
        if (look.tag == '?' || look.tag == '*' || look.tag == '+')
            move();
    }

    void seqOrChoice() throws IOException {
        match('(');
        jumpSpace();
        children();
    }

    //  attlistdecl = <!ATTLIST S Name Attdef* s? >
    void attlistdecl() throws IOException {
        logger.info("==== attlist start");
        match(attlistdecl); match(separator);
        logger.info("element = " + look);
        match(id);
        int sCount;
        // attdef = S Name S attType S defaultDecl
        while(look.tag != '>') {
            sCount = jumpSpace();
            if(look.tag == '>')break;
            if(sCount == 0)error(" miss space");
            logger.info(look);
            match(id);
            match(separator);
            attType();
            match(separator);
            defaultDecl();
        }
        jumpSpace();
        match('>');
        logger.info("==== attlist  end");
    }

    // attType = 'CDATA' | 'id' ..... | notationType | Enumeration
    void attType() throws IOException {
        String t = look.toString();
        for(String str : "CDATA|ID|IDREF|IDREFS|ENTITY|ENTITIES|NMTOKEN|NMTOKENS".split("\\|")) {
            if(t.equals(str)) {
                logger.info(t);
                match(id); return;
            }
        }

        if(t.equals("NOTATION"))
            notationType();
        else if(t.equals("("))
            enumeration();
        else error("atttype");
//        match();IOException
    }

    void defaultDecl() throws IOException {
        if(look.tag == '#') {
            match('#');
            if(look.toString().equals("REQUIRED") || look.toString().equals("IMPLIED")) {
                match(id);
            }
            else if(look.toString().equals("FIXED")) {
                logger.info("#FIXED");
                match(id); match(separator);
                attValue();
            }
        }
        else attValue();
    }


    void notationType() throws IOException{
        match(id); match(separator);
        match('(');
        jumpSpace();
        match(id);
        while(look.tag != ')') {
            jumpSpace();
            if(look.tag == ')')break;
            switch (look.tag) {
                case '|':
                    match('|'); jumpSpace();
                    match(id);
                    break;
                default: error("");
            }
            jumpSpace();
        }
        match(')');
    }

    void nmtoken() throws IOException {
        while(true) {
            String str = look.toString();
            for(int i = 0; i < str.length(); ++i) {
                if( !XmlChar.isNameChar(str.charAt(i)) )return;
            }
            move();
        }
    }

    void enumeration() throws IOException {
        logger.info("enumeration");
        match('('); jumpSpace();
        nmtoken();
        while(look.tag != ')') {
            jumpSpace();
            if(look.tag == ')')break;
            switch (look.tag) {
                case '|':
                    match('|'); jumpSpace();
                    nmtoken();
                    break;
                default:
                    error("");
            }
            jumpSpace();
        }
        match(')');
    }

//    EntityDecl ::= GEDecl | PEDecl
//[71] GEDecl ::= ' <!ENTITY' S Name S EntityDef S? ' >'
//[72] PEDecl ::= ' <!ENTITY' S '%' S Name S PEDef S? ' >'
    void entitydecl() throws IOException {
//        logger.info("==== entity start");
        String entityName;
        String entityValue;
        match(entitydecl); match(separator);
        if(look.tag == id) {
            entityName = look.toString();
            match(id); match(separator);
            entityValue = entitydef();
            int type = entityValue.charAt(0) - '0';
            // include no cursion
            try {
                entities.add(type, entityName, entityValue.substring(1));
            } catch (RuntimeException e) {
                wfc("Well-Formedness Constraint: No Recursion");
            }
        }
        else {
            match('%'); match(separator);
            entityName = look.toString();
            match(id);  match(separator);
            entityValue = pedef();
            entities.add(Entities.parameter, entityName, entityValue);
        }
        jumpSpace();
        match('>');
//        logger.info("==== entity end");
    }
    
    // entityValue = "([^&%"] | PEReference | Reference)*"
    //              | '([^&%"] | PEReference | Reference)*'
    String entityValue() throws IOException{
        StringBuilder sb = new StringBuilder();
        int tag = '"';
        if(look.tag == '\''){
            tag = '\'';
        }

        match(tag);
        while (look.tag != tag) {
            switch (look.tag) {
                case '%':
                    wfc("Well-Formedness Constraint: PEs in Internal Subset");
                    break;
                case '&':
                    sb.append(reference());
                    break;
                default:
                    sb.append(look.toString());
                    move();
            }
        }
        match(tag);
        return sb.toString();
    }
    
    // pedef = entityvalue | externalID
    String pedef () throws IOException {
        if(look.toString().equals("SYSTEM") || look.toString().equals("PUBLIC"))
            return externalId();
        else
            return entityValue();
    }
    
    // entitydef = EntityValue | (ExternalID NDataDecl?)
    String entitydef() throws IOException {
        StringBuilder sb = new StringBuilder();
        if (look.toString().equals("SYSTEM") || look.toString().equals("PUBLIC")) {
            String external =  externalId() ;
            if(look.tag == separator) {
                move();
                if(!look.toString().equals("NDATA")) {
                    // external parsed entity
                    sb.append("3");
                    sb.append(external);
                    return sb.toString();
                }
                match("NDATA");
                // unparsed entity
                sb.append("4");
                sb.append(external);
                match(separator); match(id);
                return sb.toString();
            }
            sb.append("3");
            sb.append(external);
        }
        else {
            // general internal entity
            sb.append("2");
            sb.append( entityValue() );
        }
        return sb.toString();
    }

    // NotationDecl ::= ' <!NOTATION' S Name S (ExternalID | PublicID) S? '>'
    void notationdecl () throws IOException {
        match(notationdecl);
        match(separator);
        match(id);
        match(separator);
        externalId();
        jumpSpace(); match('>');
    }


}
